// SPDX-FileCopyrightText: Adam Evyčędo
//
// SPDX-License-Identifier: GPL-3.0-or-later

package xyz.apiote.bimba.czwek.search.ui.results

import android.content.Context
import android.content.Intent
import android.hardware.Sensor
import android.hardware.SensorEvent
import android.hardware.SensorEventListener
import android.hardware.SensorManager
import android.location.Location
import android.location.LocationListener
import android.location.LocationManager
import android.os.Build
import android.os.Bundle
import android.os.Handler
import android.os.Looper
import android.os.Parcelable
import android.util.Log
import android.view.View
import android.view.ViewGroup
import androidx.activity.enableEdgeToEdge
import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.content.res.AppCompatResources
import androidx.core.view.ViewCompat
import androidx.core.view.WindowInsetsCompat
import androidx.core.view.updateLayoutParams
import androidx.core.view.updatePadding
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.repeatOnLifecycle
import androidx.recyclerview.widget.LinearLayoutManager
import com.google.android.material.snackbar.Snackbar
import com.google.openlocationcode.OpenLocationCode
import kotlinx.coroutines.Runnable
import kotlinx.coroutines.launch
import xyz.apiote.bimba.czwek.R
import xyz.apiote.bimba.czwek.api.Error
import xyz.apiote.bimba.czwek.dashboard.MainActivity
import xyz.apiote.bimba.czwek.data.exceptions.BimbaException
import xyz.apiote.bimba.czwek.data.exceptions.GeocodingFailedException
import xyz.apiote.bimba.czwek.data.exceptions.TransientException
import xyz.apiote.bimba.czwek.data.traffic.Place
import xyz.apiote.bimba.czwek.databinding.ActivityResultsBinding
import xyz.apiote.bimba.czwek.repo.Position
import xyz.apiote.bimba.czwek.repo.Queryable
import xyz.apiote.bimba.czwek.search.Query

class ResultsActivity : AppCompatActivity(), LocationListener, SensorEventListener {
	companion object {
		const val QUERY_KEY = "query"
		const val RETURN_KEY = "ret"
		private const val FEED_KEY = "feed"
		const val GEOCODING_KEY = "geocoding"
		fun getIntent(
			context: Context,
			query: Query,
			ret: Boolean = false,
			feedID: String? = null,
			withGeocoding: Boolean = false
		) =
			Intent(context, ResultsActivity::class.java).apply {
				putExtra(QUERY_KEY, query)
				putExtra(RETURN_KEY, ret)
				putExtra(FEED_KEY, feedID)
				putExtra(GEOCODING_KEY, withGeocoding)
			}
	}

	private var _binding: ActivityResultsBinding? = null
	private val binding get() = _binding!!
	private lateinit var viewModel: ResultsViewModel

	private lateinit var adapter: BimbaResultsAdapter

	private val handler = Handler(Looper.getMainLooper())
	private var runnable = Runnable {}
	private var gravity: FloatArray? = null
	private var geomagnetic: FloatArray? = null
	private var shortOLC: OpenLocationCode? = null
	private lateinit var query: Query
	private var location: Position? = null

	override fun onCreate(savedInstanceState: Bundle?) {
		enableEdgeToEdge()
		super.onCreate(savedInstanceState)
		_binding = ActivityResultsBinding.inflate(layoutInflater)
		setContentView(binding.root)
		viewModel = ViewModelProvider(this)[ResultsViewModel::class.java]

		ViewCompat.setOnApplyWindowInsetsListener(binding.resultsRecycler) { v, windowInsets ->

			windowInsets.displayCutout?.safeInsetLeft?.let {
				v.updateLayoutParams<ViewGroup.MarginLayoutParams> {
					leftMargin = it
				}
			}
			val insets = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars())
			v.updatePadding(right = insets.right, left = insets.left)
			windowInsets
		}
		ViewCompat.setOnApplyWindowInsetsListener(binding.topAppBar) { v, windowInsets ->
			val insets = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars())
			v.updatePadding(right = insets.right, left = insets.left)
			windowInsets
		}

		binding.resultsRecycler.layoutManager = LinearLayoutManager(this)
		adapter =
			BimbaResultsAdapter(layoutInflater, this, listOf(), null, null, false, getReturnResults())
		binding.resultsRecycler.adapter = adapter

		query = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
			intent.getParcelableExtra(QUERY_KEY, Query::class.java)!!
		} else {
			@Suppress("DEPRECATION")
			intent.getParcelableExtra(QUERY_KEY)!!
		}

		lifecycleScope.launch {
			repeatOnLifecycle(Lifecycle.State.STARTED) {
				viewModel.results.collect {
					if (!viewModel.startedSearching) {
						return@collect
					}

					val location = if (query.mode in setOf(
							Query.Mode.LOCATION,
							Query.Mode.LOCATION_PLUS_CODE,
							Query.Mode.POSITION
						)
					) {
						this@ResultsActivity.location?.toLocation()
					} else {
						null
					}
					updateItems(it, location, query.mode == Query.Mode.LOCATION)
				}
			}
		}

		lifecycleScope.launch {
			repeatOnLifecycle(Lifecycle.State.STARTED) {
				viewModel.finished.collect {
					if (it) {
						afterFinishSearch()
					}
				}
			}
		}

		useQuery()
	}

	private fun useQuery() {
		when (query.mode) {
			Query.Mode.LOCATION -> {
				binding.topAppBar.title = getString(R.string.stops_nearby)
				locate()
			}

			Query.Mode.LOCATION_PLUS_CODE -> {
				binding.topAppBar.title = getString(R.string.stops_near_code, query.raw)
				shortOLC = OpenLocationCode(query.raw)
				locate()
			}

			Query.Mode.UNKNOWN -> {
				try {
					query.parse(this)
					if (query.mode != Query.Mode.UNKNOWN) {
						useQuery()
					} else {
						showError(Error(0, R.string.error_unknown, R.drawable.error_other), emptyList())
					}
				} catch (e: GeocodingFailedException) {
					showError(e.cause, listOf(e))
				}
			}

			Query.Mode.POSITION -> {
				binding.topAppBar.title = getString(R.string.stops_near_code, query.raw)
				viewModel.locate(query.position!!, this)
			}

			Query.Mode.NAME -> {
				binding.topAppBar.title = getString(R.string.results_for, query.raw)
				if (intent.getBooleanExtra(GEOCODING_KEY, false)) {
					viewModel.geocode(query.raw, this)
				} else {
					viewModel.query(query.raw, this)
				}
			}
		}
	}

	private fun getReturnResults(): Boolean = intent.extras?.getBoolean(RETURN_KEY) == true

	private fun locate() {
		val sensorManager = getSystemService(SENSOR_SERVICE) as SensorManager
		val accelerometer = sensorManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER)
		val magnetometer = sensorManager.getDefaultSensor(Sensor.TYPE_MAGNETIC_FIELD)
		sensorManager.registerListener(this, accelerometer, SensorManager.SENSOR_DELAY_NORMAL)
		sensorManager.registerListener(this, magnetometer, SensorManager.SENSOR_DELAY_NORMAL)

		try {
			val locationManager = getSystemService(LOCATION_SERVICE) as LocationManager
			locationManager.requestLocationUpdates(
				LocationManager.GPS_PROVIDER, 1000 * 10, 100f, this
			)
			handler.removeCallbacks(runnable)
			runnable = Runnable {
				val exception = TransientException(
					"timeout waiting for location",
					Error(0, R.string.error_gps, R.drawable.error_gps)
				)
				showError(exception.cause, listOf(exception))
			}
			handler.postDelayed(runnable, 60 * 1000)
			locationManager.getLastKnownLocation(LocationManager.GPS_PROVIDER)
				?.let { onLocationChanged(it) }
		} catch (_: SecurityException) {
			Log.wtf(
				"locate",
				"this shouldn’t happen because we don’t start this activity without location permission"
			)
		}
	}

	override fun onLocationChanged(location: Location) {
		handler.removeCallbacks(runnable)
		if (shortOLC != null) {
			val area = shortOLC!!.recover(location.latitude, location.longitude).decode()
			viewModel.locate(Position(area.centerLatitude, area.centerLongitude), this)
		} else {
			this.location = Position(location)
			viewModel.locate(Position(location), this)
		}
	}

	override fun onAccuracyChanged(sensor: Sensor?, accuracy: Int) {}
	override fun onSensorChanged(event: SensorEvent?) {
		if (event?.sensor?.type == Sensor.TYPE_ACCELEROMETER) gravity = event.values
		if (event?.sensor?.type == Sensor.TYPE_MAGNETIC_FIELD) geomagnetic = event.values
		if (gravity != null && geomagnetic != null) {
			val r = FloatArray(9)
			val success = SensorManager.getRotationMatrix(r, FloatArray(9), gravity, geomagnetic)
			if (success) {
				val orientation = FloatArray(3)
				SensorManager.getOrientation(r, orientation)
				adapter.update((orientation[0] * 180 / Math.PI).toFloat())
			}
		}
	}

	override fun onResume() {
		super.onResume()
		if (query.mode == Query.Mode.LOCATION) {
			locate()
		}
	}

	override fun onPause() {
		super.onPause()
		val locationManager = getSystemService(LOCATION_SERVICE) as LocationManager
		locationManager.removeUpdates(this)
		handler.removeCallbacks(runnable)
		val sensorManager = getSystemService(SENSOR_SERVICE) as SensorManager
		sensorManager.unregisterListener(this)
	}

	override fun onDestroy() {
		super.onDestroy()
		val locationManager = getSystemService(LOCATION_SERVICE) as LocationManager
		locationManager.removeUpdates(this)
		handler.removeCallbacks(runnable)
	}

	private fun showError(error: Error, exceptions: List<BimbaException>) {
		binding.resultsProgress.visibility = View.GONE
		binding.resultsRecycler.visibility = View.GONE

		binding.errorImage.visibility = View.VISIBLE
		binding.errorText.visibility = View.VISIBLE
		binding.moreButton.visibility = View.VISIBLE

		binding.errorText.text = getString(error.stringResource)
		binding.errorImage.setImageDrawable(AppCompatResources.getDrawable(this, error.imageResource))
		binding.moreButton.setOnClickListener {
			BimbaException.showDialog(this, exceptions)
		}
	}

	private fun showErrorSnackbar(error: Error, exceptions: List<BimbaException>) {
		val snackbar = Snackbar.make(binding.root, error.stringResource, Snackbar.LENGTH_LONG)
		snackbar.setAction(R.string.more_info) {
			BimbaException.showDialog(this, exceptions)
		}
		snackbar.show()
	}

	private fun updateItems(queryables: List<Queryable>?, position: Location?, showArrow: Boolean) {
		binding.resultsProgress.visibility = View.GONE
		binding.resultsOverlay.visibility = View.GONE
		binding.errorImage.visibility = View.GONE
		binding.errorText.visibility = View.GONE
		binding.resultsRecycler.visibility = View.VISIBLE
		adapter.update(queryables, position, showArrow)
		binding.resultsLoading.visibility = View.VISIBLE
	}

	private fun afterFinishSearch() {
		binding.resultsLoading.visibility = View.GONE
		val searchingErrors = viewModel.consumeErrors()

		val error = when (searchingErrors.size) {
			0 -> null
			1 -> searchingErrors[0].cause
			else -> Error(0, R.string.error_multiple, R.drawable.error_other)
		}

		if (adapter.isNullOrEmpty()) {
			showError(error ?: Error(0, R.string.error_404, R.drawable.error_search), searchingErrors)
		} else if (error != null) {
			if (adapter.size() == 1) {
				if (query.mode == Query.Mode.NAME && !getReturnResults()) {
					adapter.click(0)
					// TODO can we show error?
				} else {
					showErrorSnackbar(error, searchingErrors)
				}
			} else {
				showErrorSnackbar(error, searchingErrors)
			}
		}
	}

	fun returnResult(place: Place) {
		setResult(RESULT_OK, Intent(this, MainActivity::class.java).apply {
			putExtra("PLACE", place as Parcelable)
		})
		finish()
	}
}